﻿#region Using Statements
using System;
using System.Collections.Generic;
using DirectZZT.Shared.Core;
using DirectZZT.Shared.Scripting;
using DirectZZT.Shared.World;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using DirectZZT.Shared.Graphics;
using Microsoft.Xna.Framework.Content;
#endregion

namespace DirectZZT.Shared.World.Objects
{
    /// <summary>
    /// Defines the abstract base class for any object that is placed and interacts on the board.
    /// </summary>
    [Serializable()]
    public abstract class BoardObject : BaseObject, IScriptHost
    {
        #region Fields

        private List<IScript> _scripts = new List<IScript>();

        #endregion

        #region Properties

        /// <summary>
        /// The unique Id of this game object.
        /// </summary>
        public int Id { get; set; }
        /// <summary>
        /// Gets/sets whether or not this board object is enabled.
        /// </summary>
        public bool Enabled { get; set; }
        /// <summary>
        /// Gets/sets whether or not this board object is visible.
        /// </summary>
        public bool Visible { get; set; }
        /// <summary>
        /// Gets/sets the board that this board object belongs to.
        /// </summary>
        public Board Board { get; set; }
        /// <summary>
        /// The X-position.
        /// </summary>
        public int X { get; set; }
        /// <summary>
        /// The Y-position.
        /// </summary>
        public int Y { get; set; }
        /// <summary>
        /// The update interval (in frames) of this board object.
        /// If this is zero, then it is updated each frame.
        /// Typical sane values range from 1 to 50.
        /// </summary>
        public int UpdateInterval { get; set; }
        /// <summary>
        /// The texture to use for drawing this object.
        /// If this is a non-visible object, then this is not necessary.
        /// </summary>
        public string Texture { get; protected set; }
        /// <summary>
        /// Gets/sets the color to use.
        /// </summary>
        public Color Color { get; set; }
        /// <summary>
        /// Gets/sets the texture rotation to use (if any).
        /// </summary>
        public float TextureRotation { get; set; }
        /// <summary>
        /// Gets/sets all parameters that this board object has set.
        /// Usage is same as in ZZT.
        /// See documentation for further information.
        /// </summary>
        /// <remarks>Parameters are different for each board object. Some may be null.</remarks>
        public List<string> Parameters { get; set; }
        /// <summary>
        /// Gets/sets all modifiers that this board object has set.
        /// See documentation for further information.
        /// </summary>
        /// <remarks>This is used to alter behavior/look etc. of the object (colors etc.).</remarks>
        public List<string> Modifiers { get; set; }

        /// <summary>
        /// Returns the capabilities for this board object.
        /// </summary>
        public virtual BoardObjectCapabilities Capabilities
        {
            get { return BoardObjectCapabilities.HasCollision; }
        }

        /// <summary>
        /// Gets/sets the reference of the board object that is over this board object.
        /// </summary>
        public BoardObject BoardObjectOver { get; set; }
        /// <summary>
        /// Gets/sets the reference of the board object that is under this board object.
        /// </summary>
        public BoardObject BoardObjectUnder { get; set; }

        #endregion

        #region Collision detection and handling

        /// <summary>
        /// Called when this board object has collided with another.
        /// This method is called on both "participants".
        /// </summary>
        /// <param name="args">The object that collided with us.</param>
        /// <returns>The resolve args, which determine what shall happen when the logic has concluded.</returns>
        public virtual CollisionResolveArgs OnCollide(CollisionArgs args)
        {
            CollisionResolveArgs collision = new CollisionResolveArgs()
            {
                Source = this,
                Target = args.Object,
            };
            return collision;
        }

        #endregion

        #region Methods

        /// <summary>
        /// Sets the texture rotation from degrees.
        /// </summary>
        /// <param name="degrees"></param>
        public void SetTextureRotation(float degrees)
        {
            this.TextureRotation = MathHelper.ToRadians(degrees);
        }

        /// <summary>
        /// Sets the color of this object according to one from the ZZT color palette.
        /// </summary>
        /// <param name="code"></param>
        public void SetColor(byte code)
        {
            this.Color = ZZTColor.FromCode(code);
        }

        /// <summary>
        /// Called when this object is deserialized.
        /// </summary>
        /// <param name="serializationData">The data which was used to deserialize this object.</param>
        public virtual void OnDeserialization(IDictionary<string, string> data)
        {

        }

        /// <summary>
        /// Called when this object is serialized.
        /// </summary>
        /// <param name="data">The data to store the serialization in.</param>
        public virtual void OnSerialization(IDictionary<string, string> data)
        {

        }

        /// <summary>
        /// Called when an event was sent to this <see cref="BoardObject"/>.
        /// </summary>
        /// <param name="eventName">The event name.</param>
        /// <param name="arguments">Optional parameters, appropriate for the event.</param>
        /// <returns>Whether or not the event could be dispatched.</returns>
        public virtual bool OnEvent(string eventName, params string[] arguments)
        {
            switch (eventName.ToUpperInvariant())
            {
                case "BECOME":
                case "DIE":
                    {
                        // we can use the same handler for BECOME and DIE, since DIE is just a short version of #BECOME EMPTY...

                        BoardObject obj = null;

                        if (eventName != "DIE")
                        {
                            if (arguments.Length == 1)
                            {
                                // just element
                                string element = arguments[0].ToUpperInvariant();

                                // spawn it
                                obj = BoardObject.Create(element);
                            }
                            if (arguments.Length == 2)
                            {
                                // first argument is color
                                string color = arguments[0].ToUpperInvariant();
                                string element = arguments[1].ToUpperInvariant();

                                // spawn it
                                obj = BoardObject.Create(element);
                                obj.Modifiers.Add(color);
                            }
                        }

                        // make it become (spawns it too)
                        this.Board.BecomeObject(this, obj);

                    } break;
                case "SEND":
                    {
                        if (arguments.Length == 1)
                        {
                            // go to the label in our script
                            for (int i = 0; i < _scripts.Count; i++)
                            {
                                IScript script = _scripts[i];
                                script.OnSend(arguments[0]);
                            }
                        }
                        else
                        {
                            // error!
                        }
                    } break;
                default:
                    {
                        // undispatched event received!
                        return false;
                    }
            }
            return true;
        }

        /// <summary>
        /// Called when a BECOME-action was executed on the <paramref name="source"/> object, which is replaced by this instance.
        /// </summary>
        /// <param name="source">The object whose replacement we will 'become'.</param>
        public virtual void OnBecome(BoardObject source)
        {
            this.X = source.X;
            this.Y = source.Y;
            this.Name = source.Name;
        }

        /// <summary>
        /// Updates this object.
        /// </summary>
        /// <param name="gameTime"></param>
        public virtual void Update(GameTime gameTime)
        {
        }

        /// <summary>
        /// Called by the parent <see cref="Board"/> when the global timer performed one tick.
        /// </summary>
        public virtual void OnTimerTick()
        {

        }

        /// <summary>
        /// Called when this object is added to the board.
        /// At this time, the <see cref="P:Board"/> property is already set.
        /// </summary>
        public virtual void OnAttach()
        {
            // set default color
            this.Color = ZZTColor.FromCode(ZZTColor.ColorCode.White);
            // register texture
            if (!string.IsNullOrWhiteSpace(this.Texture))
            {
                Texture2D texture = this.Board.World.TextureManager.DefaultResource;
                try
                {
                    texture = this.Board.Game.Content.Load<Texture2D>(this.Texture);
                }
                catch { }
                this.Board.World.TextureManager.RegisterResource(this.Texture, texture);
            }
        }

        /// <summary>
        /// Called when this object gets spawned on the board.
        /// </summary>
        /// <param name="modifiers">Optional modifiers that were given to when we shall spawn.</param>
        public virtual void OnSpawn(string[] modifiers)
        {
            if (modifiers != null)
            {
                this.Modifiers = new List<string>(modifiers);
            }
        }

        /// <summary>
        /// Called when this object dies (it is deleted from the board).
        /// </summary>
        public virtual void OnDie()
        {
            lock (_scripts)
            {
                // remove all scripts
                while (_scripts.Count > 0)
                {
                    DetachScript(_scripts[0]);
                }
            }
        }

        /// <summary>
        /// Returns the neighbor of this board object at the given adjacent direction.
        /// </summary>
        /// <param name="direction"></param>
        /// <returns></returns>
        public BoardObject GetNeighbor(Direction direction)
        {
            switch (direction)
            {
                case Direction.North: return GetTopNeighbor();
                case Direction.East: return GetRightNeighbor();
                case Direction.South: return GetBottomNeighbor();
                case Direction.West: return GetLeftNeighbor();
                default: return null;
            }
        }

        /// <summary>
        /// Returns the left neighbor of this board object, if any.
        /// </summary>
        /// <returns></returns>
        protected BoardObject GetLeftNeighbor()
        {
            return this.Board.GetObjectAt(this.X - 1, this.Y);
        }

        /// <summary>
        /// Returns the right neighbor of this board object, if any.
        /// </summary>
        /// <returns></returns>
        protected BoardObject GetRightNeighbor()
        {
            return this.Board.GetObjectAt(this.X + 1, this.Y);
        }

        /// <summary>
        /// Returns the top neighbor of this board object, if any.
        /// </summary>
        /// <returns></returns>
        protected BoardObject GetTopNeighbor()
        {
            return this.Board.GetObjectAt(this.X, this.Y - 1);
        }

        /// <summary>
        /// Returns the bottom neighbor of this board object, if any.
        /// </summary>
        /// <returns></returns>
        protected BoardObject GetBottomNeighbor()
        {
            return this.Board.GetObjectAt(this.X, this.Y + 1);
        }

        /// <summary>
        /// Returns whether or not attaching scripts is supported by this board object in the given feature set.
        /// </summary>
        /// <param name="featureSet"></param>
        /// <returns></returns>
        protected abstract bool CanAttachScript(FeatureSet featureSet);

        /// <summary>
        /// Returns whether or not this board object is supported in general in the given feature set.
        /// </summary>
        /// <param name="featureSet"></param>
        /// <returns></returns>
        public virtual bool IsSupportedIn(FeatureSet featureSet)
        {
            return true;
        }

        #endregion

        #region Move methods

        /// <summary>
        /// Attempts to move the board object one step towards north.
        /// If moving is not possible (blocking object etc.) then this method takes care
        /// of events and stuff. 
        /// </summary>
        /// <returns>A boolean value indicating whether or not the move operation was successful.</returns>
        protected bool MoveNorth()
        {
            return MoveRel(0, -1, Direction.South);
        }

        /// <summary>
        /// Attempts to move the board object one step towards west.
        /// If moving is not possible (blocking object etc.) then this method takes care
        /// of events and stuff. 
        /// </summary>
        /// <returns>A boolean value indicating whether or not the move operation was successful.</returns>
        protected bool MoveWest()
        {
            return MoveRel(-1, 0, Direction.East);
        }

        /// <summary>
        /// Attempts to move the board object one step towards east.
        /// If moving is not possible (blocking object etc.) then this method takes care
        /// of events and stuff. 
        /// </summary>
        /// <returns>A boolean value indicating whether or not the move operation was successful.</returns>
        protected bool MoveEast()
        {
            return MoveRel(1, 0, Direction.West);
        }

        /// <summary>
        /// Attempts to move the board object one step towards south.
        /// If moving is not possible (blocking object etc.) then this method takes care
        /// of events and stuff. 
        /// </summary>
        /// <returns>A boolean value indicating whether or not the move operation was successful.</returns>
        protected bool MoveSouth()
        {
            return MoveRel(0, 1, Direction.North);
        }

        /// <summary>
        /// Attempts to move the board object in a relative direction.
        /// If moving is not possible (blocking object etc.) then this method takes care
        /// of events and stuff. To attempt to move to a direct location, use <see cref="M:MoveAbs(int,int)"/>.
        /// </summary>
        /// <param name="dx">The delta-x to move to.</param>
        /// <param name="dy">The delta-y to move to.</param>
        /// <param name="side">The side of the collision, if any.</param>
        /// <returns>A boolean value indicating whether or not the move operation was successful.</returns>
        protected bool MoveRel(int dx, int dy, Direction side)
        {
            int x = this.X + dx;
            int y = this.Y + dy;

            BoardObject destination = this.Board.GetObjectAt(x, y);
            if (destination == null)
            {
                return this.MoveAbs(x, y);
            }
            else
            {
                // does it have collision?
                if (destination.Capabilities.HasFlag(BoardObjectCapabilities.HasCollision))
                {
                    // collide and handle resolve args
                    CollisionResolveArgs resolveDestination = destination.OnCollide(new CollisionArgs()
                    {
                        IsSource = false,
                        Object = this,
                        CollisionSide = side.Invert(),
                    });
                    CollisionResolveArgs resolveSource = this.OnCollide(new CollisionArgs()
                    {
                        IsSource = true,
                        Object = destination,
                        CollisionSide = side,
                    });

                    if (resolveDestination.RemoveSource || resolveSource.RemoveTarget)
                    {
                        this.Board.DieObject(destination);
                        // move soure to where destination was
                        return this.MoveAbs(destination.X, destination.Y);
                    }
                    if (resolveDestination.RemoveTarget || resolveSource.RemoveSource)
                    {
                        this.Board.DieObject(this);
                    }
                    return false;
                }
                else
                {
                    // go there
                    return this.MoveAbs(x, y);
                }
            }
        }

        /// <summary>
        /// Attempts to move the board object to a direct location.
        /// If moving is not possible (blocking object etc.) then this method takes care
        /// of events and stuff.
        /// </summary>
        /// <param name="x">The absolute x to move to.</param>
        /// <param name="y">The absolute y to move to.</param>
        /// <returns>A boolean value indicating whether or not the move operation was successful.</returns>
        protected bool MoveAbs(int x, int y)
        {
            BoardObject destination = this.Board.GetObjectAt(x, y);
            if (destination == null)
            {
                // great, just move there (if possible!)
                if ((x >= 0 && x < Board.BoardWidth)
                    && (y >= 0 && y < Board.BoardHeight))
                {
                    this.X = x;
                    this.Y = y;
                    return true;
                }
                return false;
            }
            else
            {
                // we can only move there if it doesn't have collision
                return !destination.Capabilities.HasFlag(BoardObjectCapabilities.HasCollision);
            }
        }

        /// <summary>
        /// Tries to move this board object to the given coordinates.
        /// </summary>
        /// <param name="x">The absolute x to move to.</param>
        /// <param name="y">The absolute y to move to.</param>
        /// <returns>Returns true if the object could be moved, and false if not.</returns>
        public bool TryMoveTo(int x, int y)
        {
            if (this.Board.GetObjectAt(x, y) == null)
            {
                this.X = x;
                this.Y = y;
                return true;
            }
            return false;
        }

        #endregion

        #region IScriptHost Members

        /// <summary>
        /// Gets/sets the executor to use for executing scripts.
        /// </summary>
        public IScriptExecutor ScriptExecutor { get; set; }

        string IScriptHost.Name
        {
            get { return this.Name; }
            set { this.Name = value; }
        }

        /// <summary>
        /// Attaches a new script to this host.
        /// </summary>
        /// <param name="script"></param>
        /// <returns>Whether or not the operation was successful.</returns>
        public bool AttachScript(IScript script)
        {
            lock (_scripts)
            {
                // is it even possible at all?
                if (!this.CanAttachScript(this.Board.World.FeatureSet))
                {
                    // TODO: "throw" special exception: FeatureException
                    return false;
                }
                // when there is already a script, then don't attach the script if the featureset doesn't support it
                if (_scripts.Count == 1 && !this.Board.World.FeatureSet.IsFeatureSupported(Feature.MoreThanOneScript))
                {
                    // TODO: "throw" special exception: FeatureException
                    // TODO: log this globally, so the user can see it!
                    return false;
                }
                _scripts.Add(script);
                script.OnAttach(this);

                // register this script at the debugger
                ScriptDebugger.RegisterScript(script);
                return true;
            }
        }

        /// <summary>
        /// Detaches/removes a script from this host.
        /// </summary>
        /// <param name="script"></param>
        /// <returns>Whether or not the operation was successful.</returns>
        public bool DetachScript(IScript script)
        {
            lock (_scripts)
            {
                script.OnDetach(this);
                _scripts.Remove(script);

                // unregister this script from the debugger
                ScriptDebugger.UnregisterScript(script);
                return true;
            }
        }

        /// <summary>
        /// Returns an enumerable that can be used to safely traverse through all the object's scripts.
        /// </summary>
        /// <returns></returns>
        public IList<IScript> GetScripts()
        {
            lock (_scripts)
            {
                return _scripts;
            }
        }

        #endregion

        #region Factory methods

        /// <summary>
        /// Attempts to create a <see cref="BoardObject"/> with the given identifier.
        /// </summary>
        /// <param name="identifier"></param>
        /// <returns></returns>
        public static BoardObject Create(string identifier)
        {
            // an EMPTY object is just nothing
            if (string.Equals(identifier, "EMPTY", StringComparison.InvariantCultureIgnoreCase))
            {
                return null;
            }

            // TODO: optimize this method... cache all mappings like in DirectZZTWorldSerializer.cs!
            // fetch all concrete types now
            ExportedType[] exports = TypeManager.Instance.GetExports();
            foreach (var item in exports)
            {
                BoardObjectAttribute attribute = BoardObjectAttribute.GetAttribute(item.Type);
                if (attribute != null && string.Equals(attribute.Identifier, identifier, StringComparison.InvariantCultureIgnoreCase))
                {
                    return (BoardObject)Activator.CreateInstance(item.Type);
                }
            }
            return null;

        }

        #endregion
    }
}
